#!/usr/bin/env python

# Scanner for the Simple LISP Dialect for Teaching
#
# Author:   Mike Murphy <mamurph@cs.clemson.edu>
# Revision: 11 January 2008
#
# Copyright 2008-2009 Clemson University
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.

import sys
# Import the scanner-related symbols we need from the shared configuration:
from shared_config import LEFT_DELIMITER, RIGHT_DELIMITER, SCAN_TYPES, \
                          OPERATOR_LIST, CompilerException
#


# Set DEBUG to True to see debugging messages. Set TRACE to True to see
# an execution trace, including state changes. These settings only affect
# this module (i.e. the scanner itself).
DEBUG=False
TRACE=False


# Debug and trace routines
def dbg(msg):
   if DEBUG:
      print >> sys.stderr, '*S:DEBUG*:', msg
#

def trace(msg):
   if TRACE:
      print >> sys.stderr, '*S:TRACE*:', msg
#


# Custom scanner exception for scan errors. It doesn't actually do anything
# different from a CompilerException, but it is a separate subclass for
# cosmetic purposes.
class ScannerException(CompilerException):
   pass
#


# Instantiate the scanner as follows: inst = Scanner(fh)
# fh is an open file object
class Scanner(object):
   def __init__(self, fh):
      self.fh = fh
      #
      # self.state is actually a pointer to a method (in C terminology) of
      # the current scanner object. This pointer is changed to change states
      # within the state machine.
      self.state = self.scan_unknown
      #
      # The most recently read character from the input stream is
      # self.curchar. When the scanner sees this string become empty, it
      # automatically reads the next character from the input.
      self.curchar = ''
      #
      # The string representation of the current token being produced is
      # self.curtok. It is reset to an empty string whenever a token is
      # accepted or an error occurs.
      self.curtok = ''
      self.toktype = SCAN_TYPES['UNKNOWN']
      self.accept = False
      self.eof = False
      self.line = 1
   #
   
   # Scanner method representing the start state of the state machine
   def scan_unknown(self):
      trace('enter scan_unknown')
      ch = self.curchar
      if ch == LEFT_DELIMITER:
         self.curtok = ch
         self.toktype = SCAN_TYPES['LDELIM']
         self.curchar = ''
         self.accept = True
         dbg('ACCEPT: left delimeter')
      elif ch == RIGHT_DELIMITER:
         self.curtok = ch
         self.toktype = SCAN_TYPES['RDELIM']
         self.curchar = ''
         self.accept = True
         dbg('ACCEPT: right delimeter')
      #
      # + and - can be integer modifiers: catch ABOVE operators
      elif ch == '+' or ch == '-':
         self.curtok = ch
         self.curchar = ''
         self.state = self.scan_intmod
         trace('STATE CHANGE: from scan_unknown to scan_intmod')
      elif ch in OPERATOR_LIST:
         self.curtok = ch
         self.toktype = SCAN_TYPES['OPER']
         self.curchar = ''
         self.state = self.scan_oper
         trace('STATE CHANGE: from scan_unknown to scan_oper')
      elif ch == '#':
         self.curchar = ''
         self.state = self.scan_comment
         trace('STATE CHANGE: from scan_unknown to scan_comment')
      elif ch == '@':
         self.curchar = ''
         self.state = self.scan_control
         self.toktype = SCAN_TYPES['CTRL']
         trace('STATE CHANGE: from scan_unknown to scan_control')
      elif ch.isdigit():
         self.curtok = ch
         self.state = self.scan_int
         self.curchar = ''
         self.toktype = SCAN_TYPES['INT']
         trace('STATE CHANGE: from scan_unknown to scan_int')
      elif ch.isalpha():
         self.curtok = ch
         self.state = self.scan_name
         self.curchar = ''
         # Don't change token type here: let scan_name handle
         trace('STATE CHANGE: from scan_unknown to scan_name')
      elif ch.isspace() or ch == '':
         self.curchar = ''
         pass
      else:
         self.curtok = ch
         self.state = self.scan_garbage
         self.curchar = ''
         trace('STATE CHANGE: from scan_unknown to scan_garbage')
      dbg('leave scan_unknown')
   #
   
   # Generic reject state: clear until whitespace or eof, then raise error
   def scan_garbage(self):
      trace('enter scan_garbage')
      ch = self.curchar
      dbg('garbage eater consuming: %s' % ch)
      self.curchar = ''
      offender = ''
      if ch.isspace() or self.eof:
         offender = self.curtok
         self.curtok = ''
         self.state = self.scan_unknown
         self.toktype = SCAN_TYPES['UNKNOWN']
         self.accept = False
         line = self.line
         if ch == '\n':   # Account for line having been incremented
            line -= 1
         trace('STATE CHANGE: from scan_garbage to scan_unknown')
         trace('leave scan_garbage by exception')
         raise ScannerException('Scan Error: %s' % offender, line)
      else:
         # Consume trash until we hit space or EOF
         self.curtok += ch
      trace('leave scan_garbage')
   
   # State for scanning operator tokens
   def scan_oper(self):
      trace('enter scan_oper')
      ch = self.curchar
      self.curchar = ''
      if ch.isspace() or self.eof:
         if self.curtok in OPERATOR_LIST:
            self.accept = True
            self.state = self.scan_unknown
            dbg('ACCEPT: operator')
            trace('STATE CHANGE: from scan_oper to scan_unknown')
         else:
            self.curchar = ch    # put current char back
            self.state = self.scan_garbage
            trace('STATE CHANGE: from scan_oper to scan_garbage')
      else:
         self.curtok += ch
      trace('leave scan_oper')
   #
   
   # State for scanning integers
   def scan_int(self):
      trace('enter scan_int')
      ch = self.curchar
      if ch.isdigit():
         self.curtok = self.curtok + ch
         self.curchar = ''
      elif ch.isspace() or ch == RIGHT_DELIMITER:
         self.accept = True
         self.state = self.scan_unknown
         dbg('ACCEPT: integer')
         trace('STATE CHANGE: from scan_int to scan_unknown')
      else:
         trace('STATE CHANGE: from scan_int to scan_garbage')
         self.state = self.scan_garbage
      trace('leave scan_int')
   #
   
   # State for scanning integer modifiers (+ and -)
   def scan_intmod(self):
      trace('enter scan_intmod')
      ch = self.curchar
      if ch.isdigit():
         if self.curtok == '+':   # strip the plus sign
            self.curtok = ch
         else:
            self.curtok += ch
         self.toktype = SCAN_TYPES['INT']
         self.state = self.scan_int
         self.curchar = ''
         trace('STATE CHANGE: from scan_intmod to scan_int')
      else:
         trace('STATE CHANGE: from scan_intmod to scan_oper')
         self.toktype = SCAN_TYPES['OPER']
         self.state = self.scan_oper
      trace('leave scan_intmod')
   
   # State for scanning names
   def scan_name(self):
      trace('enter scan_name')
      ch = self.curchar
      if ch.isalnum() or ch == '_':
         self.curtok += ch
         self.curchar = ''
      elif ch.isspace() or ch == RIGHT_DELIMITER:
         self.accept = True
         if self.curtok in OPERATOR_LIST:
            self.toktype = SCAN_TYPES['OPER']
         else:
            self.toktype = SCAN_TYPES['NAME']
         self.state = self.scan_unknown
         dbg('ACCEPT: ' + str(self.toktype))
         trace('STATE CHANGE: from scan_name to scan_unknown')
      else:
         trace('STATE CHANGE: from scan_name to scan_garbage')
         self.state = self.scan_garbage
      trace('leave scan_name')
   #
   
   # State for scanning and stripping comments
   def scan_comment(self):
      trace('enter scan_comment')
      if self.curchar == '\n':
         trace('STATE CHANGE: from scan_comment to scan_unknown')
         self.state = self.scan_unknown
      self.curchar =''
      trace('leave scan_comment')
   #
   
   # State for scanning control tokens
   def scan_control(self):
      trace('enter scan_control')
      ch = self.curchar
      self.curchar = ''
      if ch.isspace():
         self.accept = True
         self.state = self.scan_unknown
         dbg('ACCEPT: control token')
         trace('STATE CHANGE: from scan_control to scan_unknown')
      elif ch.isalpha():
         self.curtok += ch
      else:
         trace('STATE CHANGE: from scan_control to scan_garbage')
         self.curchar = ch    # put character back for garbage pickup
         self.state = self.scan_garbage
      trace('leave scan_control')
   #
   
   # Utility routine to manage scanning, including reading the next char
   # whenever self.curchar is empty, and accepting tokens
   def manage_scan(self):
      trace('enter manage_scan')
      dbg('Scan requested: starting by using state function ' + \
               self.state.__name__)
      self.accept = False
      last_line = self.line
      while not self.accept and not self.eof:
         last_line = self.line
         if self.curchar == '':
            self.curchar = self.fh.read(1)
            if self.curchar == '':
               self.eof = True
               self.fh.close()
            elif self.curchar == '\n':
               self.line += 1
         dbg("Current character: '" + self.curchar + "'")
         dbg('Calling state function: ' + self.state.__name__)
         self.state()
      #

      # self.accept == True or self.eof == True
      toktype = SCAN_TYPES['UNKNOWN']
      tokval = None

      if self.accept:
         toktype = self.toktype
         if toktype == SCAN_TYPES['INT']:
            tokval = int(self.curtok)
            # Check for 32-bit signed two's complement underflow/overflow
            if tokval < -2147483648:
               trace('leave manage_scan by exception')
               raise ScannerException('Scan Error: integer underflow: %d' % \
                                      tokval, self.line)
            elif tokval > 2147483647:
               trace('leave manage_scan by exception')
               raise ScannerException('Scan Error: integer overflow: %d' % \
                                      tokval, self.line)
         else:
            tokval = self.curtok
         self.toktype = SCAN_TYPES['UNKNOWN']
         self.curtok = ''
      elif self.eof:
         toktype = SCAN_TYPES['EOF']
         tokval = 'EOF'

      retval = (toktype, tokval, last_line)
      dbg('Scan complete: returning token tuple: ' + str(retval))
      trace('leave manage_scan')
      return retval

   def scan(self):
      return self.manage_scan()
#

if __name__ == '__main__':
   import sys, traceback
   
   sc = None
   filename = '<stdin>'
   fh = sys.stdin
   if len(sys.argv) > 1:
      filename = sys.argv[1]
      fh = open(filename, 'r')
      sc = Scanner(fh)
   else:
      print '* Simple LISP Dialect for Teaching :: Scanner Component *'
      print ''
      print 'The scanner is running in interactive mode. To exit, type'
      print 'CTRL+D (CTRL+Z on Windows) to send the EOF character.'
      print ''
      sc = Scanner(fh)
   #
   
   toktype = SCAN_TYPES['UNKNOWN']
   code = 0
   while toktype != SCAN_TYPES['EOF']:
      try:
         toktype, val, line = sc.scan()
         print 'Type: ' + str(toktype) + ', value: ' + str(val) + ', line: ' + \
             str(line)
      except KeyboardInterrupt:
         print 'Exiting on signal 2'
         sys.exit(-2)
      except CompilerException, e:
         print filename + ':' + str(e.line) + ': ' + e.message
         code = 1
      except Exception:
         traceback.print_exc()
         code = 1
   #
   
   sys.exit(code)
#
